// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: qjs_xhr.cpp
// Description: Bindings for XMLHttpRequest
//      Author: Ziming Li
//     Created: 2022-07-20
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "./qjs_bindings.h"

#include "bk_http.h"
#include "bkcommon/buffer_impl.hpp"
#include "bkcommon/response_impl.h"
#include "blinkit/app/app_impl.h"
#include "blinkit/blink/public/platform/WebTaskRunner.h"
#include "blinkit/blink/renderer/bindings/core/qjs/qjs.h"
#include "blinkit/blink/renderer/bindings/core/qjs/qjs_c_function_list_entries.h"
#include "blinkit/blink/renderer/bindings/core/script_controller.h"
#include "blinkit/blink/renderer/bindings/core/script_wrappable.h"
#include "blinkit/blink/renderer/core/frame/local_frame.h"
#include "blinkit/blink/renderer/core/loader/frame_loader_client.h"
#include "third_party/zed/include/zed/net/http_constants.hpp"
#include "third_party/zed/include/zed/net/url.hpp"
#ifdef BLINKIT_CRAWLER_ENABLED
#   include "crawler/crawler_impl.h"
#endif

namespace BlinKit {
namespace qjs {

class XMLHttpRequest final : public GCObject, public ScriptWrappable
{
public:
    static JSValue Create(JSContext *ctx, JSValue proto);
    ~XMLHttpRequest(void) override;

    enum ReadyState {
        UNSENT = 0, OPENED = 1, HEADERS_RECEIVED = 2, LOADING = 3, DONE = 4
    };
    void ChangeReadyState(JSContext *ctx, ReadyState newState);

    void Open(const std::string &method, const std::string &URL);
    void SetAsync(bool async) { m_async = async; }
    void SetRequestHeader(const std::string &name, const std::string &value) { m_headers.emplace(name, value); }
    void Send(ExceptionState &es, const std::string &body = std::string());

    int32_t Status(void) const { return nullptr != m_response ? BkGetResponseStatusCode(m_response) : 0; }
    std::string ResponseText(void) const;
private:
    XMLHttpRequest(JSContext *ctx);

    static void Call(JSContext *ctx, JSValue thisVal, const char *callback);

    void RequestComplete(BkResponse response);
    static void BKAPI RequestComplete(BkResponse response, void *userData) {
        reinterpret_cast<XMLHttpRequest *>(userData)->RequestComplete(response);
    }
    void OnRequestComplete(void);

    void RequestFailed(int errorCode);
    static void BKAPI RequestFailed(int errorCode, void *userData) {
        reinterpret_cast<XMLHttpRequest *>(userData)->RequestFailed(errorCode);
    }
    void OnRequestFailed(int errorCode);

    GCObject* ObjectForGC(void) { return this; }

    const std::shared_ptr<WebTaskRunner> m_taskRunner;
    ReadyState m_readyState = UNSENT;
#ifdef BLINKIT_CRAWLER_ENABLED
    CrawlerImpl* m_crawler = nullptr;
#endif
    JSContext *m_ctx;
    std::string m_method, m_URL;
    std::unordered_map<std::string, std::string> m_headers;
    bool m_async = true;
    BkResponse m_response = nullptr;
};

XMLHttpRequest::XMLHttpRequest(JSContext *ctx) : m_taskRunner(AppImpl::Get().taskRunner()), m_ctx(ctx)
{
    ASSERT(isMainThread());
#ifdef BLINKIT_CRAWLER_ENABLED
    ScriptController *scriptController = ScriptController::From(ctx);
    if (nullptr == scriptController)
        return;

    FrameLoaderClient *client = scriptController->GetFrame().loader().client();
    if (client->GetType() != FrameClient::Type::Crawler)
        return;

    m_headers.emplace(zed::http::header_names::UserAgent, client->userAgent().stdUtf8());
    m_crawler = static_cast<CrawlerImpl*>(client);
#endif
}

XMLHttpRequest::~XMLHttpRequest(void)
{
    ASSERT(isMainThread());
    if (nullptr != m_response)
        m_response->Release();
}

void XMLHttpRequest::Call(JSContext *ctx, JSValue thisVal, const char *callback)
{
    JSValue func = JS_GetPropertyStr(ctx, thisVal, callback);
    if (JS_IsFunction(ctx, func))
    {
        JSValue ret = JS_Call(ctx, func, thisVal, 0, nullptr);
        JS_FreeValue(ctx, ret);
    }
    JS_FreeValue(ctx, func);
}

void XMLHttpRequest::ChangeReadyState(JSContext *ctx, ReadyState newState)
{
    m_readyState = newState;
    Call(ctx, *this, "onreadystatechange");
}

JSValue XMLHttpRequest::Create(JSContext *ctx, JSValue proto)
{
    XMLHttpRequest *ret = new XMLHttpRequest(ctx);
    return ret->Wrap(ctx, proto);
}

void XMLHttpRequest::OnRequestComplete(void)
{
    ChangeReadyState(m_ctx, DONE);
    Call(m_ctx, *this, "onload");
    Call(m_ctx, *this, "onloadend");
}

void XMLHttpRequest::OnRequestFailed(int errorCode)
{
    ChangeReadyState(m_ctx, DONE);

    const char *callback =
        BK_ERR_REQUEST_TIMEOUT == errorCode
        ? "ontimeout"
        : "onerror"
        ;
    Call(m_ctx, *this, callback);
    Call(m_ctx, *this, "onloadend");
}

void XMLHttpRequest::Open(const std::string &method, const std::string &URL)
{
    m_method = method;
    m_URL = URL;
}

void XMLHttpRequest::RequestComplete(BkResponse response)
{
    response->Retain();
    auto task = [this, response] {
        m_response = response;
        OnRequestComplete();
    };

    if (m_async)
        m_taskRunner->postTask(BLINK_FROM_HERE, std::move(task));
    else
        task();
}

void XMLHttpRequest::RequestFailed(int errorCode)
{
    auto task = [this, errorCode] {
        OnRequestFailed(errorCode);
    };

    if (m_async)
        m_taskRunner->postTask(BLINK_FROM_HERE, std::move(task));
    else
        task();
}

std::string XMLHttpRequest::ResponseText(void) const
{
    std::string ret;
    if (nullptr != m_response)
        BkGetResponseData(m_response, BK_RESPONSE_BODY, BufferImpl::Wrap(ret));
    return ret;
}

void XMLHttpRequest::Send(ExceptionState &es, const std::string &body)
{
    if (OPENED != m_readyState)
    {
        es.throwDOMException(InvalidStateError, "Invalid state");
        return;
    }

    BkRequestClient client = { 0 };
    client.SizeOfStruct    = sizeof(BkRequestClient);
    client.UserData        = this;
    client.RequestComplete = RequestComplete;
    client.RequestFailed   = RequestFailed;

    BkRequest request = BkCreateRequest(m_URL.c_str(), &client);
    BkSetRequestMethod(request, m_method.c_str());
    for (const auto &it : m_headers)
        BkSetRequestHeader(request, it.first.c_str(), it.second.c_str());

    if (!body.empty())
        BkSetRequestBody(request, body.data(), body.length());

#ifdef BLINKIT_CRAWLER_ENABLED
    if (nullptr != m_crawler)
        m_crawler->ModifyRequest(m_URL.c_str(), request);
#endif

    if (m_async)
    {
        if (BK_ERR_SUCCESS == BkPerformRequest(request, nullptr))
            return;

        es.throwDOMException(NetworkError, "Invalid request");
    }
    else
    {
        BkPerformRequestSynchronously(request);
    }
}

/**
 * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
 */

static JSValue GetResponseText(JSContext *ctx, JSValueConst thisVal)
{
    XMLHttpRequest *xhr = ScriptWrappable::Cast<XMLHttpRequest>(thisVal);
    std::string ret = xhr->ResponseText();
    return JS_NewStringLen(ctx, ret.data(), ret.length());
}

static JSValue GetStatus(JSContext *ctx, JSValueConst thisVal)
{
    XMLHttpRequest *xhr = ScriptWrappable::Cast<XMLHttpRequest>(thisVal);
    return JS_NewInt32(ctx, xhr->Status());
}

static JSValue Open(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid method");
    if (!JS_IsString(argv[1]))
        return JS_ThrowTypeError(ctx, "Invalid URL");

    XMLHttpRequest *xhr = ScriptWrappable::Cast<XMLHttpRequest>(thisVal);
    xhr->Open(ToStdString(ctx, argv[0]), ToStdString(ctx, argv[1]));
    if (argc > 2)
        xhr->SetAsync(JS_ToBool(ctx, argv[2]));
    xhr->ChangeReadyState(ctx, XMLHttpRequest::OPENED);

    return JS_UNDEFINED;
}

static JSValue SetRequestHeader(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    XMLHttpRequest *xhr = ScriptWrappable::Cast<XMLHttpRequest>(thisVal);
    xhr->SetRequestHeader(ToStdString(ctx, argv[0]), ToStdString(ctx, argv[1]));
    return JS_UNDEFINED;
}

static JSValue Send(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    XMLHttpRequest *xhr = ScriptWrappable::Cast<XMLHttpRequest>(thisVal);

    Context c(*ctx);
    if (argc > 0)
        xhr->Send(c, ToStdString(ctx, argv[0]));
    else
        xhr->Send(c);

    if (c.hadException())
        return c.ReturnException();
    return JS_UNDEFINED;
}

static JSValue Constructor(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    JSValue ret;

    JSValue proto = JS_GetPropertyStr(ctx, thisVal, "prototype");
    ret = XMLHttpRequest::Create(ctx, proto);
    JS_FreeValue(ctx, proto);

    return ret;
}

static JSValue CreateXMLHttpRequestPrototype(JSContext *ctx)
{
    static const JSCFunctionListEntry Funcs[] = {
        CFunctionEntry("open", 2, Open),
        CGetSetEntry("responseText", GetResponseText, nullptr),
        CFunctionEntry("send", 0, Send),
        CFunctionEntry("setRequestHeader", 2, SetRequestHeader),
        CGetSetEntry("status", GetStatus, nullptr)
    };

    JSValue ret = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, ret, Funcs, std::size(Funcs));
    return ret;
}

void AddXMLHttpRequest(JSContext *ctx, JSValue global)
{
    static const char XHR[] = "XMLHttpRequest";

    JSValue ctor = JS_NewCFunction2(ctx, Constructor, XHR, 0, JS_CFUNC_constructor, 0);

    JSValue proto = CreateXMLHttpRequestPrototype(ctx);
    JS_SetConstructor(ctx, ctor, proto);
    JS_FreeValue(ctx, proto);

    static const JSCFunctionListEntry Funcs[] = {
        CPropInt32Entry("UNSENT", XMLHttpRequest::UNSENT, 0),
        CPropInt32Entry("OPENED", XMLHttpRequest::OPENED, 0),
        CPropInt32Entry("HEADERS_RECEIVED", XMLHttpRequest::HEADERS_RECEIVED, 0),
        CPropInt32Entry("LOADING", XMLHttpRequest::LOADING, 0),
        CPropInt32Entry("DONE", XMLHttpRequest::DONE, 0)
    };
    JS_SetPropertyFunctionList(ctx, ctor, Funcs, std::size(Funcs));

    JS_SetPropertyStr(ctx, global, XHR, ctor);
}

} // namespace qjs
} // namespace BlinKit
